This notebook explores looking at the connectivity and memory requirements for Nengo models


In [1]:
%matplotlib inline
import pylab
import seaborn
import numpy as np
import pandas

import netlist
reload(netlist)
import pprint


/home/tcstewar/env/work/local/lib/python2.7/site-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
  warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')

We start with a simple model of semantic association, which is often identified as one of the most difficult structures in Spaun. Initially, we'll look at a 1-Dimensional version (just so we can visualize it), but then we'll scale up to 512 dimensions.


In [44]:
import ctn_benchmark.spa.memory
model = ctn_benchmark.spa.memory.SemanticMemory().make_model(D=1)

# extract the graph from a nengo model.  This returns:
#   ensembles    (groups of x->A->y neurons)
#   nodes        (non-neural components used for organization)
#   connections  (edges)
e, n, c = netlist.compute_graph(model)

# compute some basic stats
pprint.pprint(netlist.calc_stats(e, n, c))

# draw the graph
netlist.plot_graph(e, n, c, size=(10,10))


{'memory': 1921, 'messages': 59, 'neurons': 950, 'values': 39}
Out[44]:
%3 139642448855184 1->1 50 139642528011344 1 139642448855184->139642528011344 1 139642506433424 1->1 100 139642484518160 4 139642506433424->139642484518160 1 139642732246032 1->1 100 139642484517328 4 139642732246032->139642484517328 1 139642506433936 1->1 100 139642506433936->139642484518160 1 139642732245264 1->1 100 139642732245264->139642484517328 1 139642506432912 1->1 100 139642506432912->139642484518160 1 139642507853328 1->1 50 139642526390672 1 139642507853328->139642526390672 1 139642506434960 1->1 100 139642506434960->139642484518160 1 139642732226960 1->1 100 139642732226960->139642484517328 1 139642513090000 1->1 50 139642494486160 1 139642513090000->139642494486160 1 139642473759312 1->1 100 139642473759312->139642484517328 1 139642473762768 1 139642526390672->139642473762768 1->1 *(1x1) h(t) 139642473760912 4 139642484518160->139642473760912 4->4 *(1) 139642473761808 4 139642473762768->139642473761808 1->4 *(4x1) 139642473760720 1 139642473762192 4 139642473760720->139642473762192 1->4 *(4x1) 139642528011344->139642473760720 1->1 *(1x1) h(t) 139642434217424 1 139642473760912->139642434217424 4->1 *(1x4) 139642434217104 0->1 139642520090896 1 139642434217104->139642520090896 1 139642513091856 1 139642513091856->139642513090000 1 139642520090896->139642507853328 1 139642506436240 4 139642473761808->139642506436240 4->4 *(1) 139642473760528 4 139642473761808->139642473760528 4->4 *(1) 139642434218576 0->1 139642448856336 1 139642434218576->139642448856336 1 139642434217424->139642513091856 1->1 *(1x1) h(t) 139642506436240->139642506433424 1 139642506436240->139642506433936 1 139642506436240->139642506432912 1 139642506436240->139642506434960 1 139642448856336->139642448855184 1 139642494486160->139642513091856 1 h(t) 139642506436304 1->0 139642494486160->139642506436304 1 h(t) 139642473762192->139642506436240 4->4 *(1) 139642473762192->139642473760528 4->4 *(1) 139642473760528->139642732246032 1 139642473760528->139642732245264 1 139642473760528->139642732226960 1 139642473760528->139642473759312 1 139642484517328->139642473760912 4->4 *(1)

This graph shows the raw structure of a Nengo model.

Groups of neurons are shown as circles. They are labeled with the dimensionality of their inputs (x) and outputs (y), along with the number of neurons. So each of the groups have 1-dimensional input and 1-dimensional output (1->1). The ones in the middle have 100 neurons each, and the ones on the left and right have 50 neurons each.

Inputs and outputs to and from the model are marked as double-outlined squares.

The diamonds are intermediate values that are generally useful during the design stage, but may be compiled out by the hardware, depending on the situation. We can think of them as just storage for a vector, rather than directly connecting from one group of neurons to the next. Sometimes these are completely useless artifacts of how the programmer happened to design the model. For example, the diamond 1s on either side of the 1->1 / 50 ensembles do absolutely nothing, just passing their values along. We can think of this as something like the network w->x->A->y->z, where w is the same size as x and is connected to it with an identity matrix (and same for y and z). However, sometimes these are conceptually useful, and can have impact on message routing. For example, consider the diamond 4s on either side of the column of neurons in the middle of the graph. These aggregate 4 separate values (the inputs/outputs from four 1-dimensional groups of neurons) into a single vector. We can now make connections into and out of that grouped vector, rather than making multiple connections into and out of the individual groups.

Finally, we have the connections (edges) themselves. There are two types of connections being shown here. The simple message-passing edges are just labeled with a single number indicating the dimensionality of the message. The other type of edge also multiplies the message before sending it. This multiplication can either be by a matrix (indicated as *(m,n)) or by a scalar (indicated as *(1)). There also may be a low-pass filter, indicates as h(t).

While this is the default Nengo representation, it's not quite what we want here. We can also transform this graph slightly to make all the edges just simple message-passing, and introduce a new type of component that just does some multiplications (and maybe a filter) on its inputs. This is done below, indicated by a square. Inside the square we indicate the dimensionality of the input and output, the number of multiplies that must be performed total, and the number of low-pass filters applied (if any).


In [3]:
import ctn_benchmark.spa.memory
model = ctn_benchmark.spa.memory.SemanticMemory().make_model(D=1)

e, n, c = netlist.compute_graph(model)
netlist.simplify_conns(e, n, c)
pprint.pprint(netlist.calc_stats(e, n, c))

netlist.plot_graph(e, n, c, size=(10,10))


{'memory_d': 950,
 'memory_e': 950,
 'memory_m': 21,
 'messages': 62,
 'n_neurons': 950,
 'n_values': 51}
Out[3]:
%3 139642732685136 1->1 100 139642732302672 4->4 M: 4 139642732685136->139642732302672 1 139642732983184 1->1 50 139642732982800 1->1 M: 1 F: 1 139642732983184->139642732982800 1 139642732685904 1->1 100 139642732685904->139642732302672 1 139642732919568 1->1 50 139642733028304 1->2 F: 2 139642732919568->139642733028304 1 139642732683344 1->1 100 139642732686992 4->4 M: 4 139642732683344->139642732686992 1 139642733027600 1->1 50 139642732985104 1->1 M: 1 F: 1 139642733027600->139642732985104 1 139642732685392 1->1 100 139642732685392->139642732302672 1 139642732683856 1->1 100 139642732683856->139642732686992 1 139642732683600 1->1 100 139642732683600->139642732686992 1 139642732685648 1->1 100 139642732685648->139642732302672 1 139642733031184 1->1 100 139642733031184->139642732686992 1 139642732919312 1->4 M: 4 139642732985104->139642732919312 1 139642733030288 4->8 M: 8 139642732684816 4 139642733030288->139642732684816 4 139642733030864 4 139642733030288->139642733030864 4 139642732686608 1->0 139642733028304->139642732686608 1 139642732920208 1 139642733028304->139642732920208 1 139642733030672 4->1 M: 4 139642732302672->139642733030672 4 139642732304912 0->1 139644386057232 1 139642732304912->139644386057232 1 139642732303440 1->1 M: 1 F: 1 139642732303440->139642732920208 1 139642733030608 4->8 M: 8 139642733030608->139642732684816 4 139642733030608->139642733030864 4 139642732684816->139642732685136 1 139642732684816->139642732685904 1 139642732684816->139642732685392 1 139642732684816->139642732685648 1 139642733030672->139642732303440 1 139644386057232->139642733027600 1 139642732920208->139642732919568 1 139642732304080 0->1 139642732983824 1 139642732304080->139642732983824 1 139642733030864->139642732683344 1 139642733030864->139642732683856 1 139642733030864->139642732683600 1 139642733030864->139642733031184 1 139642732919376 1->4 M: 4 139642732982800->139642732919376 1 139642732919376->139642733030608 4 139642732686992->139642733030672 4 139642732983824->139642732983184 1 139642732919312->139642733030288 4

This sort of reorganizing can be thought of as a sort of compilier optimization. Making changes like this won't affect the behaviour of the model, ubt may make it more (or less) efficient in terms of the hardware. Some optimizations are very obvious: for example, in the above graph, after the first group of neurons in the top left, we have three boxes, each of which perform matrix multiplications. All of these can get merged together into one matrix, just by multiplying those matrices together.

In fact, they could even get rolled all the way back into the decoder for the neural group itself! That is, instead of doing x->A->y->p->q->r as it is now, we coule instead multiply all those matrices together and get x->A->r directly. (Note that even though there's a filter on the first box, we can move that filter to the end without changing anything, because low-pass filters are linear).

So, this sort of optimization can be interesting, and so far we've seen it to be very hardware-specific.

The above graph is for a semantic memory of size 1 (i.e. it stores associated 1-dimensional vectors). For human-level cognitive tasks, we need somewhere around 512-dimensional vectors. Let's see how the memory requirements scale with size:


In [53]:
import ctn_benchmark.spa.memory

stats = []
Ds = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
for D in Ds:
    model = ctn_benchmark.spa.memory.SemanticMemory().make_model(D=D)

    e, n, c = netlist.compute_graph(model)
    netlist.simplify_conns(e, n, c)
    s = netlist.calc_stats(e, n, c)
    s['D'] = D
    stats.append(s)

In [54]:
df = pandas.DataFrame(stats)
df


Out[54]:
D memory messages neurons values
0 1 1921 62 950 51
1 2 4466 124 1900 102
2 4 9798 200 3000 164
3 8 27878 352 5200 288
4 16 93702 656 9600 536
5 32 231520 9520 18400 9288
6 64 479712 19024 36000 18568
7 128 1080544 41048 71200 40144
8 256 2355936 85096 141600 83296
9 512 5496544 182408 282400 178816

In [55]:
pylab.semilogy(df['memory'], label='memory')
pylab.semilogy(df['neurons'], label='neurons')
pylab.semilogy(df['messages'], label='messages')
pylab.semilogy(df['values'], label='values')
pylab.xticks(np.arange(len(Ds)), Ds)
pylab.legend(loc='best')
pylab.xlabel('size of semantic memory')
pylab.show()


We can also take a look at the distribution of the ensembles in the network. For this network, we have a very regular structure.


In [100]:
model = ctn_benchmark.spa.memory.SemanticMemory().make_model(D=512)
e, n, c = netlist.compute_graph(model)

ens = {}
for ee in e.values():
    key = (ee['size_in'], ee['n_neurons'], ee['size_out'])
    ens[key] = ens.get(key, 0) + 1
pprint.pprint(ens)


{(1, 100, 1): 2056, (16, 800, 16): 96}

That is, we have 2056 groups of neurons that are 1 input, 100 neurons, and 1 output, and then we have 96 groups that have 16 inputs, 800 neurons, and 16 outputs.

Now let's try this with a different model. Here is the Basal Ganglia model that was benchmarked in the SpiNNaker paper.


In [56]:
model = ctn_benchmark.nengo.SPASequence().make_model(n_actions=5)
e, n, c = netlist.compute_graph(model)
netlist.simplify_conns(e, n, c)
pprint.pprint(netlist.calc_stats(e, n, c))

netlist.plot_graph(e, n, c, size=(10,10))


{'memory': 59607, 'messages': 453, 'neurons': 4350, 'values': 386}
Out[56]:
%3 139642501219152 1->2 100 139642484099920 5->5 M: 5 139642501219152->139642484099920 1 139642501219664 5 139642501219152->139642501219664 1 139642472419536 1->2 100 139642515414416 5 139642472419536->139642515414416 1 139642482524816 5->10 M: 10 F: 10 139642472419536->139642482524816 1 139642472420368 1->2 100 139642472420368->139642515414416 1 139642472420368->139642482524816 1 139642526049360 1->2 100 139642482526928 5->10 M: 50 F: 10 139642526049360->139642482526928 1 139643479206928 5 139642526049360->139643479206928 1 139642516759312 16->16 800 139642487371792 32->37 M: 160 F: 37 139642516759312->139642487371792 16 139642490983824 16->16 800 139642490983824->139642487371792 16 139642471196240 1->1 50 139642471195728 5->10 M: 25 F: 10 139642471196240->139642471195728 1 139642494103176 1->32 M: 32 F: 32 139642471196240->139642494103176 1 139642501221648 1->2 100 139642501221648->139642484099920 1 139642501221648->139642501219664 1 139642478646992 1->2 100 139642478646992->139642482526928 1 139642478646992->139643479206928 1 139642472421456 1->2 100 139642472421456->139642515414416 1 139642472421456->139642482524816 1 139642523715856 1->2 100 139643459765072 5->5 M: 5 F: 5 139642523715856->139643459765072 1 139642523716624 5 139642523715856->139642523716624 1 139642501220944 1->2 100 139642501220944->139642484099920 1 139642501220944->139642501219664 1 139642472419408 1->2 100 139642472419408->139642515414416 1 139642472419408->139642482524816 1 139642471197776 1->1 50 139642471197776->139642471195728 1 139642494147112 1->32 M: 32 F: 32 139642471197776->139642494147112 1 139642471198672 1->1 50 139642471198672->139642471195728 1 139642494148232 1->32 M: 32 F: 32 139642471198672->139642494148232 1 139642518068368 1->2 100 139642493242640 5 139642518068368->139642493242640 1 139642507853136 5->5 M: 5 F: 5 139642518068368->139642507853136 1 139643479207184 1->2 100 139643479207184->139642482526928 1 139643479207184->139643479206928 1 139642502044240 1->1 50 139642488216384 1->32 M: 32 F: 32 139642502044240->139642488216384 1 139642502044240->139642471195728 1 139642519541584 1->2 100 139642519541584->139642493242640 1 139642519541584->139642507853136 1 139643479203920 1->2 100 139643479203920->139642484099920 1 139643479203920->139642501219664 1 139642471196752 1->1 50 139642471196752->139642471195728 1 139642494145992 1->32 M: 32 F: 32 139642471196752->139642494145992 1 139642501221904 1->2 100 139642501221904->139642484099920 1 139642501221904->139642501219664 1 139642523717392 1->2 100 139642523717392->139643459765072 1 139642523717392->139642523716624 1 139643459765136 1->2 100 139643459765136->139642493242640 1 139643459765136->139642507853136 1 139642515414928 1->2 100 139642515414928->139642515414416 1 139642515414928->139642482524816 1 139642524041168 1->2 100 139642524041168->139642493242640 1 139642524041168->139642507853136 1 139642523713616 1->2 100 139642523713616->139643459765072 1 139642523713616->139642523716624 1 139642523713744 1->2 100 139642523713744->139643459765072 1 139642523713744->139642523716624 1 139642478645520 1->2 100 139642478645520->139642482526928 1 139642478645520->139643479206928 1 139642523713936 1->2 100 139642523713936->139643459765072 1 139642523713936->139642523716624 1 139642508562896 1->2 100 139642508562896->139642493242640 1 139642508562896->139642507853136 1 139642478646032 1->2 100 139642478646032->139642482526928 1 139642478646032->139643479206928 1 139642507850448 32 139642488216384->139642507850448 32 139642508563472 5 139642508563472->139642518068368 1 139642508563472->139642519541584 1 139642508563472->139643459765136 1 139642508563472->139642524041168 1 139642508563472->139642508562896 1 139642519965264 5->0 139642471195728->139642519965264 5 139642502042192 5 139642471195728->139642502042192 5 139642471197136 0->5 139642471197136->139642502042192 5 139642472422416 5->15 M: 10 139642487371792->139642472422416 1 139642487371792->139642472422416 1 139642487371792->139642472422416 1 139642487371792->139642472422416 1 139642487371792->139642472422416 1 139642487371792->139642507850448 32 139642472422416->139642508563472 5 139642478647760 5 139642472422416->139642478647760 5 139642493243024 5 139642472422416->139642493243024 5 139642494145992->139642507850448 32 139642494147112->139642507850448 32 139642472421520 5 139642472421520->139642472419536 1 139642472421520->139642472420368 1 139642472421520->139642472421456 1 139642472421520->139642472419408 1 139642472421520->139642515414928 1 139642494148232->139642507850448 32 139642478647760->139642526049360 1 139642478647760->139642478646992 1 139642478647760->139643479207184 1 139642478647760->139642478645520 1 139642478647760->139642478646032 1 139642482526928->139642472421520 5 139643479204112 5 139642482526928->139643479204112 5 139642515414352 5->5 F: 5 139642484099920->139642515414352 5 139642515414352->139642502042192 5 139643459765072->139642472421520 5 139642493243024->139642523715856 1 139642493243024->139642523717392 1 139642493243024->139642523713616 1 139642493243024->139642523713744 1 139642493243024->139642523713936 1 139642519963792 0->32 139642519963792->139642507850448 32 139642494103176->139642507850448 32 139643479204112->139642501219152 1 139643479204112->139642501221648 1 139643479204112->139642501220944 1 139643479204112->139643479203920 1 139643479204112->139642501221904 1 139642507853136->139643479204112 5 139642502042192->139642471196240 1 139642502042192->139642471197776 1 139642502042192->139642471198672 1 139642502042192->139642502044240 1 139642502042192->139642471196752 1 139642482524816->139642478647760 5 139642482524816->139643479204112 5 139642507850448->139642516759312 16 139642507850448->139642490983824 16

In [60]:
import ctn_benchmark.spa.memory

stats = []
actions = [5, 10, 20, 50, 100, 200]
for n_actions in actions:
    model = ctn_benchmark.nengo.SPASequence().make_model(n_actions=n_actions, D=512)
    e, n, c = netlist.compute_graph(model)
    netlist.simplify_conns(e, n, c)
    s = netlist.calc_stats(e, n, c)
    s['n_actions'] = n_actions
    stats.append(s)

In [61]:
df = pandas.DataFrame(stats)
df


Out[61]:
memory messages n_actions neurons values
0 832407 4773 5 28350 4226
1 845757 7498 10 31100 6916
2 872907 12948 20 36600 12296
3 957957 29298 50 53100 28436
4 1111707 56548 100 80600 55336
5 1464207 111048 200 135600 109136

In [64]:
pylab.semilogy(df['memory'], label='memory')
pylab.semilogy(df['neurons'], label='neurons')
pylab.semilogy(df['messages'], label='messages')
pylab.semilogy(df['values'], label='values')
pylab.xticks(np.arange(len(actions)), actions)
pylab.legend(loc='best')
pylab.xlabel('number of actions to choose from')
pylab.show()



In [102]:
model = model = ctn_benchmark.nengo.SPASequence().make_model(n_actions=200, D=512)
e, n, c = netlist.compute_graph(model)

ens = {}
for ee in e.values():
    key = (ee['size_in'], ee['n_neurons'], ee['size_out'])
    ens[key] = ens.get(key, 0) + 1
pprint.pprint(ens)


{(1, 50, 1): 200, (1, 100, 2): 1000, (16, 800, 16): 32}

Here, we have 200 groups that are 1->50->1, 1000 groups that are 1->100->2, and 32 that are 18->800->16.

Now let's take a look at this for Spaun! I've pre-computed the netlist for this, as it's huge. And it's also so huge that we're not going to try to plot the actual diagram.


In [87]:
import shelve
db = shelve.open('spaun_netlist')
e = db['e']
n = db['n']
c = db['c']
db.close()

But, we can compute the stats on it.


In [88]:
netlist.calc_stats(e, n, c)


Out[88]:
{'memory': 30455151, 'messages': 535172, 'neurons': 4311350, 'values': 3255050}

This is a much smaller memory requirement than we'd quoted before! (down to 30M from 400M). What happened here?

One possibility is that there's a bug in my code for extracting the netlist, and I'm looking into that. But there have also been a lot of changes to Spaun since we last measured this number. In particular, it looks like it's using a lot more smaller ensembles. This is specific to this particular version of Spaun (and this particular model). I'm looking into grabbing a few more largish models to see if I can get a wider variety of components.

However, in the meantime, we can look at the sizes of the neuron groups in Spaun:


In [99]:
ens = {}
for ee in e.values():
    key = (ee['size_in'], ee['n_neurons'], ee['size_out'])
    ens[key] = ens.get(key, 0) + 1
pprint.pprint(ens)


{(1, 20, 0): 7,
 (1, 25, 2): 24,
 (1, 40, 0): 9,
 (1, 50, 0): 83,
 (1, 50, 1): 61552,
 (1, 50, 2): 3004,
 (1, 50, 3): 30,
 (1, 75, 2): 8224,
 (1, 100, 0): 2,
 (1, 100, 1): 803,
 (1, 100, 2): 1194,
 (1, 150, 1): 1,
 (1, 150, 2): 2,
 (1, 200, 0): 1,
 (1, 200, 1): 1,
 (1, 500, 0): 1,
 (1, 500, 1): 50,
 (2, 150, 2): 1,
 (2, 150, 4): 1024,
 (2, 9000, 3): 6,
 (3, 5000, 3): 1,
 (6, 7000, 3): 2,
 (6, 7000, 6): 1}

The giant ensembles at the bottom are the new motor system, which they've apparantly increased to be larger than I thought they'd done. But other than that, it's all 1-D and 2-D ensembles! So that might help with the mapping for Spaun in particular, but our other models still do require larger 16-D ensembles.

(Note: the ensembles with 0 outputs are ones that could get compiled out....)


In [ ]: